<!DOCTYPE html>
<html>
<head><meta name="generator" content="Hexo 3.8.0">
  <meta charset="utf-8">
  

  
  <title>shiro会话管理 | Hexo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="description" content="shiro会话管理会话管理shiro提供了完整的企业级会话管理功能，不依赖于底层容器（如web容器tomcat）,不管JavaSE还是JavaEE环境都可以使用，提供了会话管理，会话事件监听，会话存储/持久化，容器无关的集群，失效/过期支持，对web的透明支持，SSO单点登录的支持等特性。即直接使用shiro的会话管理可以直接替换如web容器的会话管理。4">
<meta name="keywords" content="shiro">
<meta property="og:type" content="article">
<meta property="og:title" content="shiro会话管理">
<meta property="og:url" content="http://yoursite.com/2018/10/12/2018-03-16-shiro会话管理/index.html">
<meta property="og:site_name" content="Hexo">
<meta property="og:description" content="shiro会话管理会话管理shiro提供了完整的企业级会话管理功能，不依赖于底层容器（如web容器tomcat）,不管JavaSE还是JavaEE环境都可以使用，提供了会话管理，会话事件监听，会话存储/持久化，容器无关的集群，失效/过期支持，对web的透明支持，SSO单点登录的支持等特性。即直接使用shiro的会话管理可以直接替换如web容器的会话管理。4">
<meta property="og:locale" content="default">
<meta property="og:image" content="https://img.w3cschool.cn/attachments/image/wk/shiro/16.png">
<meta property="og:updated_time" content="2018-11-01T14:24:04.935Z">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="shiro会话管理">
<meta name="twitter:description" content="shiro会话管理会话管理shiro提供了完整的企业级会话管理功能，不依赖于底层容器（如web容器tomcat）,不管JavaSE还是JavaEE环境都可以使用，提供了会话管理，会话事件监听，会话存储/持久化，容器无关的集群，失效/过期支持，对web的透明支持，SSO单点登录的支持等特性。即直接使用shiro的会话管理可以直接替换如web容器的会话管理。4">
<meta name="twitter:image" content="https://img.w3cschool.cn/attachments/image/wk/shiro/16.png">
  
    <link rel="alternate" href="/org/atom.xml" title="Hexo" type="application/atom+xml">
  
  
    <link rel="icon" href="/favicon.png">
  
  
    <link href="//fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet" type="text/css">
  
  <link rel="stylesheet" href="/org/css/style.css">
</head>
</html>
<body>
  <div id="container">
    <div id="wrap">
      <header id="header">
  <div id="banner"></div>
  <div id="header-outer" class="outer">
    <div id="header-title" class="inner">
      <h1 id="logo-wrap">
        <a href="/org/" id="logo">Hexo</a>
      </h1>
      
    </div>
    <div id="header-inner" class="inner">
      <nav id="main-nav">
        <a id="main-nav-toggle" class="nav-icon"></a>
        
          <a class="main-nav-link" href="/org/">Home</a>
        
          <a class="main-nav-link" href="/org/archives">Archives</a>
        
      </nav>
      <nav id="sub-nav">
        
          <a id="nav-rss-link" class="nav-icon" href="/org/atom.xml" title="RSS Feed"></a>
        
        <a id="nav-search-btn" class="nav-icon" title="Search"></a>
      </nav>
      <div id="search-form-wrap">
        <form action="//google.com/search" method="get" accept-charset="UTF-8" class="search-form"><input type="search" name="q" class="search-form-input" placeholder="Search"><button type="submit" class="search-form-submit">&#xF002;</button><input type="hidden" name="sitesearch" value="http://yoursite.com"></form>
      </div>
    </div>
  </div>
</header>
      <div class="outer">
        <section id="main"><article id="post-2018-03-16-shiro会话管理" class="article article-type-post" itemscope="" itemprop="blogPost">
  <div class="article-meta">
    <a href="/org/2018/10/12/2018-03-16-shiro会话管理/" class="article-date">
  <time datetime="2018-10-12T09:03:30.000Z" itemprop="datePublished">2018-10-12</time>
</a>
    
  </div>
  <div class="article-inner">
    
    
      <header class="article-header">
        
  
    <h1 class="article-title" itemprop="name">
      shiro会话管理
    </h1>
  

      </header>
    
    <div class="article-entry" itemprop="articleBody">
      
        <h2 id="shiro会话管理"><a href="#shiro会话管理" class="headerlink" title="shiro会话管理"></a>shiro会话管理</h2><h3 id="会话管理"><a href="#会话管理" class="headerlink" title="会话管理"></a>会话管理</h3><p>shiro提供了完整的企业级会话管理功能，不依赖于底层容器（如web容器tomcat）,不管JavaSE还是JavaEE环境都可以使用，提供了会话管理，会话事件监听，会话存储/持久化，容器无关的集群，失效/过期支持，对web的透明支持，SSO单点登录的支持等特性。即直接使用shiro的会话管理可以直接替换如web容器的会话管理。4</p>
<a id="more"></a>
<h2 id="会话"><a href="#会话" class="headerlink" title="会话"></a>会话</h2><p>所谓会话，即用户访问应用时保持的连接关系，在多次交互中应用能够识别出当前访问的用户是谁，且可以在多次交互中保存一些数据。如访问一些网站时登录成功后，网站可以记住用户，且在退出之前都可以识别当前用户是谁。</p>
<p>Shiro 的会话支持不仅可以在普通的 JavaSE 应用中使用，也可以在 JavaEE 应用中使用，如 web 应用。且使用方式是一致的。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">login(&quot;classpath:shiro.ini&quot;, &quot;zhang&quot;, &quot;123&quot;);</span><br><span class="line">Subject subject = SecurityUtils.getSubject();</span><br><span class="line">Session session = subject.getSession();</span><br></pre></td></tr></table></figure>
<p>登录成功后使用 Subject.getSession() 即可获取会话；其等价于 Subject.getSession(true)，即如果当前没有创建 Session 对象会创建一个；另外 Subject.getSession(false)，如果当前没有创建 Session 则返回 null（不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session）。</p>
<p><code>session.getId();</code></p>
<p>获取当前会话的唯一标识。</p>
<p><code>session.getHost();</code></p>
<p>获取当前 Subject 的主机地址，该地址是通过 HostAuthenticationToken.getHost() 提供的。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">session.getTimeout();</span><br><span class="line">session.setTimeout(毫秒);</span><br></pre></td></tr></table></figure>
<p>获取 / 设置当前 Session 的过期时间；如果不设置默认是会话管理器的全局过期时间。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">session.getStartTimestamp();</span><br><span class="line">session.getLastAccessTime();</span><br></pre></td></tr></table></figure>
<p>获取会话的启动时间及最后访问时间；如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间；如果是 Web 应用，每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">session.touch();</span><br><span class="line">session.stop();</span><br></pre></td></tr></table></figure>
<p>更新会话最后访问时间及销毁会话；当 Subject.logout() 时会自动调用 stop 方法来销毁会话。如果在 web 中，调用 javax.servlet.http.HttpSession. invalidate() 也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">session.setAttribute(&quot;key&quot;, &quot;123&quot;);</span><br><span class="line">Assert.assertEquals(&quot;123&quot;, session.getAttribute(&quot;key&quot;));</span><br><span class="line">session.removeAttribute(&quot;key&quot;);</span><br></pre></td></tr></table></figure>
<p>设置 / 获取 / 删除会话属性；在整个会话范围内都可以对这些属性进行操作。</p>
<p>Shiro 提供的会话可以用于 JavaSE/JavaEE 环境，不依赖于任何底层容器，可以独立使用，是完整的会话模块。</p>
<h2 id="会话管理器"><a href="#会话管理器" class="headerlink" title="会话管理器"></a>会话管理器</h2><p>会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件，顶层组件 SecurityManager 直接继承了 SessionManager，且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager，DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。</p>
<p>SecurityManager 提供了如下接口：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Session start(SessionContext context); //启动会话</span><br><span class="line">Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话</span><br></pre></td></tr></table></figure>
<p>另外用于 Web 环境的 WebSessionManager 又提供了如下接口：</p>
<p><code>boolean isServletContainerSessions();// 是否使用 Servlet 容器的会话</code></p>
<p>Shiro 还提供了 ValidatingSessionManager 用于验资并过期会话：</p>
<p><code>void validateSessions();// 验证所有会话是否过期</code></p>
<p>Shiro 提供了三个默认实现：</p>
<p><strong>DefaultSessionManager</strong>：DefaultSecurityManager 使用的默认实现，用于 JavaSE 环境；<br><strong>ServletContainerSessionManager</strong>：DefaultWebSecurityManager 使用的默认实现，用于 Web 环境，其直接使用 Servlet 容器的会话；<br><strong>DefaultWebSessionManager</strong>：用于 Web 环境的实现，可以替代 ServletContainerSessionManager，自己维护着会话，直接废弃了 Servlet 容器的会话管理。</p>
<p>替换 SecurityManager 默认的 SessionManager 可以在 ini 中配置（shiro.ini）：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[main]</span><br><span class="line">sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager</span><br><span class="line">securityManager.sessionManager=$sessionManager</span><br></pre></td></tr></table></figure>
<p>Web 环境下的 ini 配置 (shiro-web.ini)：</p>
<p><code>&lt;!--EndFragment--&gt;</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[main]</span><br><span class="line">sessionManager=org.apache.shiro.web.session.mgt.ServletContainerSessionManager</span><br><span class="line">securityManager.sessionManager=$sessionManager</span><br></pre></td></tr></table></figure>
<p>另外可以设置会话的全局过期时间（毫秒为单位），默认 30 分钟：</p>
<p><code>sessionManager. globalSessionTimeout=1800000</code></p>
<p>默认情况下 globalSessionTimeout 将应用给所有 Session。可以单独设置每个 Session 的 timeout 属性来为每个 Session 设置其超时时间。</p>
<p>另外如果使用 ServletContainerSessionManager 进行会话管理，Session 的超时依赖于底层 Servlet 容器的超时时间，可以在 web.xml 中配置其会话的超时时间（分钟为单位）：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;session-config&gt;</span><br><span class="line">  &lt;session-timeout&gt;30&lt;/session-timeout&gt;</span><br><span class="line">&lt;/session-config&gt;</span><br></pre></td></tr></table></figure>
<p>在 Servlet 容器中，默认使用 JSESSIONID Cookie 维护会话，且会话默认是跟容器绑定的；在某些情况下可能需要使用自己的会话机制，此时我们可以使用 DefaultWebSessionManager 来维护会话：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie</span><br><span class="line">sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager</span><br><span class="line">sessionIdCookie.name=sid</span><br><span class="line">\#sessionIdCookie.domain=sishuok.com</span><br><span class="line">\#sessionIdCookie.path=</span><br><span class="line">sessionIdCookie.maxAge=1800</span><br><span class="line">sessionIdCookie.httpOnly=true</span><br><span class="line">sessionManager.sessionIdCookie=$sessionIdCookie</span><br><span class="line">sessionManager.sessionIdCookieEnabled=true</span><br><span class="line">securityManager.sessionManager=$sessionManager</span><br></pre></td></tr></table></figure>
<ul>
<li>sessionIdCookie 是 sessionManager 创建会话 Cookie 的模板：</li>
<li>sessionIdCookie.name：设置 Cookie 名字，默认为 JSESSIONID；</li>
<li>sessionIdCookie.domain：设置 Cookie 的域名，默认空，即当前访问的域名；</li>
<li>sessionIdCookie.path：设置 Cookie 的路径，默认空，即存储在域名根下；</li>
<li>sessionIdCookie.maxAge：设置 Cookie 的过期时间，秒为单位，默认 - 1 表示关闭浏览器时过期 Cookie；</li>
<li>sessionIdCookie.httpOnly：如果设置为 true，则客户端不会暴露给客户端脚本代码，使用 HttpOnly cookie 有助于减少某些类型的跨站点脚本攻击；此特性需要实现了 Servlet 2.5 MR6 及以上版本的规范的 Servlet 容器支持；</li>
<li>sessionManager.sessionIdCookieEnabled：是否启用 / 禁用 Session Id Cookie，默认是启用的；如果禁用后将不会设置 Session Id Cookie，即默认使用了 Servlet 容器的 JSESSIONID，且通过 URL 重写（URL 中的 “;JSESSIONID=id” 部分）保存 Session Id。</li>
</ul>
<p>另外我们可以如 “sessionManager. sessionIdCookie.name=sid” 这种方式操作 Cookie 模板。</p>
<h2 id="会话监听器"><a href="#会话监听器" class="headerlink" title="会话监听器"></a>会话监听器</h2><p>会话监听器用于监听会话创建、过期及停止事件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">public class MySessionListener1 implements SessionListener &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public void onStart(Session session) &#123;//会话创建时触发</span><br><span class="line">        System.out.println(&quot;会话创建：&quot; + session.getId());</span><br><span class="line">    &#125;</span><br><span class="line">    @Override</span><br><span class="line">    public void onExpiration(Session session) &#123;//会话过期时触发</span><br><span class="line">        System.out.println(&quot;会话过期：&quot; + session.getId());</span><br><span class="line">    &#125;</span><br><span class="line">    @Override</span><br><span class="line">    public void onStop(Session session) &#123;//退出/会话过期时触发</span><br><span class="line">        System.out.println(&quot;会话停止：&quot; + session.getId());</span><br><span class="line">    &#125;  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>如果只想监听某一个事件，可以继承 SessionListenerAdapter 实现：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public class MySessionListener2 extends SessionListenerAdapter &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public void onStart(Session session) &#123;</span><br><span class="line">        System.out.println(&quot;会话创建：&quot; + session.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>在 shiro-web.ini 配置文件中可以进行如下配置设置会话监听器：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1</span><br><span class="line">sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2</span><br><span class="line">sessionManager.sessionListeners=$sessionListener1,$sessionListener2</span><br></pre></td></tr></table></figure>
<h2 id="会话存储-持久化"><a href="#会话存储-持久化" class="headerlink" title="会话存储 / 持久化"></a>会话存储 / 持久化</h2><p>Shiro 提供 SessionDAO 用于会话的 CRUD，即 DAO（Data Access Object）模式实现：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">//如DefaultSessionManager在创建完session后会调用该方法；如保存到关系数据库/文件系统/NoSQL数据库；即可以实现会话的持久化；返回会话ID；主要此处返回的ID.equals(session.getId())；</span><br><span class="line">Serializable create(Session session);</span><br><span class="line">//根据会话ID获取会话</span><br><span class="line">Session readSession(Serializable sessionId) throws UnknownSessionException;</span><br><span class="line">//更新会话；如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用</span><br><span class="line">void update(Session session) throws UnknownSessionException;</span><br><span class="line">//删除会话；当会话过期/会话停止（如用户退出时）会调用</span><br><span class="line">void delete(Session session);</span><br><span class="line">//获取当前所有活跃用户，如果用户量多此方法影响性能</span><br><span class="line">Collection&lt;Session&gt; getActiveSessions();</span><br></pre></td></tr></table></figure>
<p>Shiro 内嵌了如下 SessionDAO 实现：</p>
<p><img src="https://img.w3cschool.cn/attachments/image/wk/shiro/16.png" alt="img"></p>
<p>AbstractSessionDAO 提供了 SessionDAO 的基础实现，如生成会话 ID 等；CachingSessionDAO 提供了对开发者透明的会话缓存的功能，只需要设置相应的 CacheManager 即可；MemorySessionDAO 直接在内存中进行会话维护；而 EnterpriseCacheSessionDAO 提供了缓存功能的会话维护，默认情况下使用 MapCache 实现，内部使用 ConcurrentHashMap 保存缓存的会话。</p>
<p>可以通过如下配置设置 SessionDAO：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO</span><br><span class="line">sessionManager.sessionDAO=$sessionDAO</span><br></pre></td></tr></table></figure>
<p>Shiro 提供了使用 Ehcache 进行会话存储，Ehcache 可以配合 TerraCotta 实现容器无关的分布式集群。</p>
<p>首先在 pom.xml 里添加如下依赖：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;shiro-ehcache&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.2.2&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure>
<p>接着配置 shiro-web.ini 文件：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO</span><br><span class="line">sessionDAO. activeSessionsCacheName=shiro-activeSessionCache</span><br><span class="line">sessionManager.sessionDAO=$sessionDAO</span><br><span class="line">cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager</span><br><span class="line">cacheManager.cacheManagerConfigFile=classpath:ehcache.xml</span><br><span class="line">securityManager.cacheManager = $cacheManager</span><br></pre></td></tr></table></figure>
<ul>
<li>sessionDAO. activeSessionsCacheName：设置 Session 缓存名字，默认就是 shiro-activeSessionCache；</li>
<li>cacheManager：缓存管理器，用于管理缓存的，此处使用 Ehcache 实现；</li>
<li>cacheManager.cacheManagerConfigFile：设置 ehcache 缓存的配置文件；</li>
<li>securityManager.cacheManager：设置 SecurityManager 的 cacheManager，会自动设置实现了 CacheManagerAware 接口的相应对象，如 SessionDAO 的 cacheManager；</li>
</ul>
<p>然后配置 ehcache.xml：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;cache name=&quot;shiro-activeSessionCache&quot;</span><br><span class="line">       maxEntriesLocalHeap=&quot;10000&quot;</span><br><span class="line">       overflowToDisk=&quot;false&quot;</span><br><span class="line">       eternal=&quot;false&quot;</span><br><span class="line">       diskPersistent=&quot;false&quot;</span><br><span class="line">       timeToLiveSeconds=&quot;0&quot;</span><br><span class="line">       timeToIdleSeconds=&quot;0&quot;</span><br><span class="line">       statistics=&quot;true&quot;/&gt;</span><br></pre></td></tr></table></figure>
<p>Cache 的名字为 shiro-activeSessionCache，即设置的 sessionDAO 的 activeSessionsCacheName 属性值。</p>
<p>另外可以通过如下 ini 配置设置会话 ID 生成器：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sessionIdGenerator=org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator</span><br><span class="line">sessionDAO.sessionIdGenerator=$sessionIdGenerator</span><br></pre></td></tr></table></figure>
<p>用于生成会话 ID，默认就是 JavaUuidSessionIdGenerator，使用 java.util.UUID 生成。</p>
<p>如果自定义实现 SessionDAO，继承 CachingSessionDAO 即可：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">public class MySessionDAO extends CachingSessionDAO &#123;</span><br><span class="line">    private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();</span><br><span class="line">     protected Serializable doCreate(Session session) &#123;</span><br><span class="line">        Serializable sessionId = generateSessionId(session);</span><br><span class="line">        assignSessionId(session, sessionId);</span><br><span class="line">        String sql = &quot;insert into sessions(id, session) values(?,?)&quot;;</span><br><span class="line">        jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session));</span><br><span class="line">        return session.getId();</span><br><span class="line">    &#125;</span><br><span class="line">protected void doUpdate(Session session) &#123;</span><br><span class="line">    if(session instanceof ValidatingSession &amp;&amp; !((ValidatingSession)session).isValid()) &#123;</span><br><span class="line">        return; //如果会话过期/停止 没必要再更新了</span><br><span class="line">    &#125;</span><br><span class="line">        String sql = &quot;update sessions set session=? where id=?&quot;;</span><br><span class="line">        jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());</span><br><span class="line">    &#125;</span><br><span class="line">    protected void doDelete(Session session) &#123;</span><br><span class="line">        String sql = &quot;delete from sessions where id=?&quot;;</span><br><span class="line">        jdbcTemplate.update(sql, session.getId());</span><br><span class="line">    &#125;</span><br><span class="line">    protected Session doReadSession(Serializable sessionId) &#123;</span><br><span class="line">        String sql = &quot;select session from sessions where id=?&quot;;</span><br><span class="line">        List&lt;String&gt; sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId);</span><br><span class="line">        if(sessionStrList.size() == 0) return null;</span><br><span class="line">        return SerializableUtils.deserialize(sessionStrList.get(0));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>doCreate/doUpdate/doDelete/doReadSession 分别代表创建 / 修改 / 删除 / 读取会话；此处通过把会话序列化后存储到数据库实现；接着在 shiro-web.ini 中配置：</p>
<p><code>sessionDAO=com.github.zhangkaitao.shiro.chapter10.session.dao.MySessionDAO</code></p>
<p>其他设置和之前一样，因为继承了 CachingSessionDAO；所有在读取时会先查缓存中是否存在，如果找不到才到数据库中查找。</p>
<h2 id="会话验证"><a href="#会话验证" class="headerlink" title="会话验证"></a>会话验证</h2><p>Shiro 提供了会话验证调度器，用于定期的验证会话是否已过期，如果过期将停止会话；出于性能考虑，一般情况下都是获取会话时来验证会话是否过期并停止会话的；但是如在 web 环境中，如果用户不主动退出是不知道会话是否过期的，因此需要定期的检测会话是否过期，Shiro 提供了会话验证调度器 SessionValidationScheduler 来做这件事情。</p>
<p>可以通过如下 ini 配置开启会话验证：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">sessionValidationScheduler=org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler</span><br><span class="line">sessionValidationScheduler.interval = 3600000</span><br><span class="line">sessionValidationScheduler.sessionManager=$sessionManager</span><br><span class="line">sessionManager.globalSessionTimeout=1800000</span><br><span class="line">sessionManager.sessionValidationSchedulerEnabled=true</span><br><span class="line">sessionManager.sessionValidationScheduler=$sessionValidationScheduler</span><br></pre></td></tr></table></figure>
<ul>
<li>sessionValidationScheduler：会话验证调度器，sessionManager 默认就是使用 ExecutorServiceSessionValidationScheduler，其使用 JDK 的 ScheduledExecutorService 进行定期调度并验证会话是否过期；</li>
<li>sessionValidationScheduler.interval：设置调度时间间隔，单位毫秒，默认就是 1 小时；</li>
<li>sessionValidationScheduler.sessionManager：设置会话验证调度器进行会话验证时的会话管理器；</li>
<li>sessionManager.globalSessionTimeout：设置全局会话超时时间，默认 30 分钟，即如果 30 分钟内没有访问会话将过期；</li>
<li>sessionManager.sessionValidationSchedulerEnabled：是否开启会话验证器，默认是开启的；</li>
<li>sessionManager.sessionValidationScheduler：设置会话验证调度器，默认就是使用 ExecutorServiceSessionValidationScheduler。</li>
</ul>
<p>Shiro 也提供了使用 Quartz 会话验证调度器：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sessionValidationScheduler=org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler</span><br><span class="line">sessionValidationScheduler.sessionValidationInterval = 3600000</span><br><span class="line">sessionValidationScheduler.sessionManager=$sessionManager</span><br></pre></td></tr></table></figure>
<p>使用时需要导入 shiro-quartz 依赖</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">     &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt;</span><br><span class="line">     &lt;artifactId&gt;shiro-quartz&lt;/artifactId&gt;</span><br><span class="line">     &lt;version&gt;1.2.2&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure>
<p>如上会话验证调度器实现都是直接调用 AbstractValidatingSessionManager 的 validateSessions 方法进行验证，其直接调用 SessionDAO 的 getActiveSessions 方法获取所有会话进行验证，如果会话比较多，会影响性能；可以考虑如分页获取会话并进行验证，如 com.github.zhangkaitao.shiro.chapter10.session.scheduler.MySessionValidationScheduler</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">//分页获取会话并验证</span><br><span class="line">String sql = &quot;select session from sessions limit ?,?&quot;;</span><br><span class="line">int start = 0; //起始记录</span><br><span class="line">int size = 20; //每页大小</span><br><span class="line">List&lt;String&gt; sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);</span><br><span class="line">while(sessionList.size() &gt; 0) &#123;</span><br><span class="line">  for(String sessionStr : sessionList) &#123;</span><br><span class="line">    try &#123;</span><br><span class="line">      Session session = SerializableUtils.deserialize(sessionStr);</span><br><span class="line">      Method validateMethod = </span><br><span class="line">        ReflectionUtils.findMethod(AbstractValidatingSessionManager.class, </span><br><span class="line">            &quot;validate&quot;, Session.class, SessionKey.class);</span><br><span class="line">      validateMethod.setAccessible(true);</span><br><span class="line">      ReflectionUtils.invokeMethod(validateMethod, </span><br><span class="line">        sessionManager, session, new DefaultSessionKey(session.getId()));</span><br><span class="line">    &#125; catch (Exception e) &#123;</span><br><span class="line">        //ignore</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"> start = start + size;</span><br><span class="line">  sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>其直接改造自 ExecutorServiceSessionValidationScheduler，如上代码是验证的核心代码，可以根据自己的需求改造此验证调度器器；ini 的配置和之前的类似。</p>
<p>如果在会话过期时不想删除过期的会话，可以通过如下 ini 配置进行设置：</p>
<p><code>sessionManager.deleteInvalidSessions=false</code></p>
<p>默认是开启的，在会话过期后会调用 SessionDAO 的 delete 方法删除会话：如会话时持久化存储的，可以调用此方法进行删除。</p>
<p>如果是在获取会话时验证了会话已过期，将抛出 InvalidSessionException；因此需要捕获这个异常并跳转到相应的页面告诉用户会话已过期，让其重新登录，如可以在 web.xml 配置相应的错误页面：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;error-page&gt;</span><br><span class="line">    &lt;exception-type&gt;org.apache.shiro.session.InvalidSessionException&lt;/exception-type&gt;</span><br><span class="line">    &lt;location&gt;/invalidSession.jsp&lt;/location&gt;</span><br><span class="line">&lt;/error-page&gt;</span><br></pre></td></tr></table></figure>
<h2 id="sessionFactory"><a href="#sessionFactory" class="headerlink" title="sessionFactory"></a>sessionFactory</h2><p>sessionFactory 是创建会话的工厂，根据相应的 Subject 上下文信息来创建会话；默认提供了 SimpleSessionFactory 用来创建 SimpleSession 会话。</p>
<p>首先自定义一个 Session：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public class OnlineSession extends SimpleSession &#123;</span><br><span class="line">    public static enum OnlineStatus &#123;</span><br><span class="line">        on_line(&quot;在线&quot;), hidden(&quot;隐身&quot;), force_logout(&quot;强制退出&quot;);</span><br><span class="line">        private final String info;</span><br><span class="line">        private OnlineStatus(String info) &#123;</span><br><span class="line">            this.info = info;</span><br><span class="line">        &#125;</span><br><span class="line">        public String getInfo() &#123;</span><br><span class="line">            return info;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    private String userAgent; //用户浏览器类型</span><br><span class="line">    private OnlineStatus status = OnlineStatus.on_line; //在线状态</span><br><span class="line">    private String systemHost; //用户登录时系统IP</span><br><span class="line">    //省略其他</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>OnlineSession 用于保存当前登录用户的在线状态，支持如离线等状态的控制。</p>
<p>接着自定义 SessionFactory：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public class OnlineSessionFactory implements SessionFactory &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public Session createSession(SessionContext initData) &#123;</span><br><span class="line">        OnlineSession session = new OnlineSession();</span><br><span class="line">        if (initData != null &amp;&amp; initData instanceof WebSessionContext) &#123;</span><br><span class="line">            WebSessionContext sessionContext = (WebSessionContext) initData;</span><br><span class="line">            HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();</span><br><span class="line">            if (request != null) &#123;</span><br><span class="line">                session.setHost(IpUtils.getIpAddr(request));</span><br><span class="line">                session.setUserAgent(request.getHeader(&quot;User-Agent&quot;));</span><br><span class="line">                session.setSystemHost(request.getLocalAddr() + &quot;:&quot; + request.getLocalPort());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        return session;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>OnlineSession 用于保存当前登录用户的在线状态，支持如离线等状态的控制。</p>
<p>接着自定义 SessionFactory：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public class OnlineSessionFactory implements SessionFactory &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public Session createSession(SessionContext initData) &#123;</span><br><span class="line">        OnlineSession session = new OnlineSession();</span><br><span class="line">        if (initData != null &amp;&amp; initData instanceof WebSessionContext) &#123;</span><br><span class="line">            WebSessionContext sessionContext = (WebSessionContext) initData;</span><br><span class="line">            HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();</span><br><span class="line">            if (request != null) &#123;</span><br><span class="line">                session.setHost(IpUtils.getIpAddr(request));</span><br><span class="line">                session.setUserAgent(request.getHeader(&quot;User-Agent&quot;));</span><br><span class="line">                session.setSystemHost(request.getLocalAddr() + &quot;:&quot; + request.getLocalPort());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        return session;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>OnlineSession 用于保存当前登录用户的在线状态，支持如离线等状态的控制。</p>
<p>接着自定义 SessionFactory：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">public class OnlineSessionFactory implements SessionFactory &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public Session createSession(SessionContext initData) &#123;</span><br><span class="line">        OnlineSession session = new OnlineSession();</span><br><span class="line">        if (initData != null &amp;&amp; initData instanceof WebSessionContext) &#123;</span><br><span class="line">            WebSessionContext sessionContext = (WebSessionContext) initData;</span><br><span class="line">            HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();</span><br><span class="line">            if (request != null) &#123;</span><br><span class="line">                session.setHost(IpUtils.getIpAddr(request));</span><br><span class="line">                session.setUserAgent(request.getHeader(&quot;User-Agent&quot;));</span><br><span class="line">                session.setSystemHost(request.getLocalAddr() + &quot;:&quot; + request.getLocalPort());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        return session;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>根据会话上下文创建相应的 OnlineSession。</p>
<p>最后在 shiro-web.ini 配置文件中配置：</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sessionFactory=org.apache.shiro.session.mgt.OnlineSessionFactory</span><br><span class="line">sessionManager.sessionFactory=$sessionFactory</span><br></pre></td></tr></table></figure>

      
    </div>
    <footer class="article-footer">
      <a data-url="http://yoursite.com/2018/10/12/2018-03-16-shiro会话管理/" data-id="cjoztxu2y000u1wijh1mj8t5e" class="article-share-link">Share</a>
      
      
  <ul class="article-tag-list"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/org/tags/shiro/">shiro</a></li></ul>

    </footer>
  </div>
  
    
<nav id="article-nav">
  
    <a href="/org/2018/10/12/2018-03-13-shiro身份验证/" id="article-nav-newer" class="article-nav-link-wrap">
      <strong class="article-nav-caption">Newer</strong>
      <div class="article-nav-title">
        
          shiro身份验证
        
      </div>
    </a>
  
  
    <a href="/org/2018/10/12/2018-03-17-shiro Spring集成/" id="article-nav-older" class="article-nav-link-wrap">
      <strong class="article-nav-caption">Older</strong>
      <div class="article-nav-title">shiro spring集成</div>
    </a>
  
</nav>

  
</article>

</section>
        
          <aside id="sidebar">
  
    

  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Tags</h3>
    <div class="widget">
      <ul class="tag-list"><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/SVN/">SVN</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/Spring-secrity/">Spring-secrity</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/hexo/">hexo</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/hibenrate/">hibenrate</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/jekyll/">jekyll</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/jenkins/">jenkins</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/redis/">redis</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/shiro/">shiro</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/spingMVC/">spingMVC</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/spring-cloud/">spring cloud</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/spring-cloud/">spring-cloud</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/spring-secrity/">spring-secrity</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/springMVC/">springMVC</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/springboot/">springboot</a></li><li class="tag-list-item"><a class="tag-list-link" href="/org/tags/生活/">生活</a></li></ul>
    </div>
  </div>


  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Tag Cloud</h3>
    <div class="widget tagcloud">
      <a href="/org/tags/SVN/" style="font-size: 12px;">SVN</a> <a href="/org/tags/Spring-secrity/" style="font-size: 10px;">Spring-secrity</a> <a href="/org/tags/hexo/" style="font-size: 10px;">hexo</a> <a href="/org/tags/hibenrate/" style="font-size: 10px;">hibenrate</a> <a href="/org/tags/jekyll/" style="font-size: 10px;">jekyll</a> <a href="/org/tags/jenkins/" style="font-size: 10px;">jenkins</a> <a href="/org/tags/redis/" style="font-size: 16px;">redis</a> <a href="/org/tags/shiro/" style="font-size: 18px;">shiro</a> <a href="/org/tags/spingMVC/" style="font-size: 10px;">spingMVC</a> <a href="/org/tags/spring-cloud/" style="font-size: 10px;">spring cloud</a> <a href="/org/tags/spring-cloud/" style="font-size: 10px;">spring-cloud</a> <a href="/org/tags/spring-secrity/" style="font-size: 12px;">spring-secrity</a> <a href="/org/tags/springMVC/" style="font-size: 14px;">springMVC</a> <a href="/org/tags/springboot/" style="font-size: 20px;">springboot</a> <a href="/org/tags/生活/" style="font-size: 10px;">生活</a>
    </div>
  </div>

  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Archives</h3>
    <div class="widget">
      <ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/org/archives/2018/11/">November 2018</a></li><li class="archive-list-item"><a class="archive-list-link" href="/org/archives/2018/10/">October 2018</a></li></ul>
    </div>
  </div>


  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Recent Posts</h3>
    <div class="widget">
      <ul>
        
          <li>
            <a href="/org/2018/11/27/redis07-zookeeper-kafka集群部署以及如何使用简单介绍/">redis07-zookeeper+kafka集群部署以及如何使用简单介绍</a>
          </li>
        
          <li>
            <a href="/org/2018/11/13/redis06-cluster实现高可用性/">redis06-cluster实现高可用性</a>
          </li>
        
          <li>
            <a href="/org/2018/11/12/redis05-在项目中搭建读写分-高可用-多master的redis-cluster集群/">redis05-在项目中搭建读写分+高可用+多master的redis cluster集群</a>
          </li>
        
          <li>
            <a href="/org/2018/11/05/在项目中用经典的三节点方式部署哨兵集群-笔记/">在项目中用经典的三节点方式部署哨兵集群-笔记</a>
          </li>
        
          <li>
            <a href="/org/2018/11/05/redis哨兵的多个核心底层原理-笔记/">redis哨兵的多个核心底层原理-笔记</a>
          </li>
        
      </ul>
    </div>
  </div>

  
</aside>
        
      </div>
      <footer id="footer">
  
  <div class="outer">
    <div id="footer-info" class="inner">
      &copy; 2018 John Doe<br>
      Powered by <a href="http://hexo.io/" target="_blank">Hexo</a>
    </div>
  </div>
</footer>
    </div>
    <nav id="mobile-nav">
  
    <a href="/org/" class="mobile-nav-link">Home</a>
  
    <a href="/org/archives" class="mobile-nav-link">Archives</a>
  
</nav>
    

<script src="//code.jquery.com/jquery-2.0.3.min.js"></script>


  <link rel="stylesheet" href="/org/fancybox/jquery.fancybox.css">
  <script src="/org/fancybox/jquery.fancybox.pack.js"></script>


<script src="/org/js/script.js"></script>



  </div>
</body>
</html>